home *** CD-ROM | disk | FTP | other *** search
/ Personal Computer World 2008 February / PCWFEB08.iso / Software / Freeware / Miro 1.0 / Miro_Installer.exe / xulrunner / python / template.py < prev    next >
Encoding:
Python Source  |  2007-11-12  |  21.6 KB  |  559 lines

  1. # Miro - an RSS based video player application
  2. # Copyright (C) 2005-2007 Participatory Culture Foundation
  3. #
  4. # This program is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation; either version 2 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  17.  
  18. #
  19. # Contains runtime template code
  20.  
  21. import os
  22. import config
  23. import eventloop
  24. from templatehelper import quoteattr, escape, attrPattern, rawAttrPattern, resourcePattern, generateId
  25. from templateoptimize import HTMLChangeOptimizer
  26. from xhtmltools import urlencode
  27. from template_compiler import checkU
  28. from itertools import chain
  29. import logging
  30. import util
  31.  
  32. # FIXME add support for onlyBody parameter for static templates so we
  33. #       don't need to strip the outer HTML
  34. import re
  35. HTMLPattern = re.compile("^.*<body.*?>(.*)</body\s*>", re.S)
  36.  
  37. if os.environ.has_key('DEMOCRACY_RECOMPILE_TEMPLATES'):
  38.     import template_compiler
  39.     import resources
  40.     template_compiler.setResourcePath(resources.path(''))
  41.  
  42. ###############################################################################
  43. #### Public interface                                                      ####
  44. ###############################################################################
  45.  
  46. # Fill the template in the given file in the template directory using
  47. # the information in the dictionary given by 'data'. If the template
  48. # contains dynamic views, call update methods on the provided
  49. # domHandler function object as necessary in the future, passing a
  50. # string that should be executed in the context of the page to update
  51. # it.  Returns a tuple: a string giving the HTML or XML that resulted
  52. # from filling the template, and a "template handle" whose
  53. # unlinkTemplate() method you should call when you no longer want to
  54. # receive Javascript callbacks.
  55. def fillTemplate(filename, domHandler, platform, eventCookie, bodyTagExtra="", top = True, onlyBody = False, *args, **kargs):
  56.     # FIXME add support for "onlyBody"
  57.     if os.environ.has_key('DEMOCRACY_RECOMPILE_TEMPLATES'):
  58.         (tcc, handle) = template_compiler.compileTemplate(filename)
  59.         exec tcc.getOutput() in locals()
  60.         return fillTemplate(domHandler, platform, eventCookie, bodyTagExtra, *args, **kargs)
  61.     else:
  62.         filename = filename.replace('/','.').replace('\\','.').replace('-','_')
  63.         components = filename.split('.')
  64.         mod = __import__("compiled_templates.%s"%filename)
  65.         for comp in components:
  66.             mod = getattr(mod,comp)
  67.         return mod.fillTemplate(domHandler, platform, eventCookie, bodyTagExtra, *args, **kargs)
  68.  
  69. # As fillTemplate, but no Javascript calls are made, and no template
  70. # handle is returned, only the HTML or XML as a string. Effectively,
  71. # you get a static snapshot of the page at the time the call is made.
  72. def fillStaticTemplate(filename, platform='', eventCookie='noCookie', bodyTagExtra="", onlyBody=False, *args, **kargs):
  73.     (tch, handle) = fillTemplate(filename, None, platform, eventCookie, bodyTagExtra, *args, **kargs)
  74.     handle.unlinkTemplate()
  75.     rv = tch.read()
  76.     if onlyBody: # FIXME we should support "onlyBody" in fillTemplate
  77.         rv = HTMLPattern.match(rv).group(1)
  78.     return rv
  79.  
  80. def queueDOMChange(func, name):
  81.     """Queue function that does a bunch of DOM updates to a display.
  82.  
  83.     What happens is a little weird, we queue a call in the main gui thread
  84.     loop, then we queue a second call in the backend loop.  If that call does
  85.     any DOM updates like changeItems, addItemBefore, etc., those will almost
  86.     certainly be queued back into the main loop.
  87.  
  88.     The rational for this is that if the main loop is busy it's better to wait
  89.     for it to be idle if we have a bunch of changes.  That way we can group
  90.     things together and have them happen all at once.  This may seem like it
  91.     adds a bunch of latency, but this doesn't seem to be the case,
  92.     addUrgentCall() happens quickly and if we are waiting on the gui thread,
  93.     that means we're doing some other kind of gui update.
  94.     """
  95.  
  96.     import frontend
  97.     try:
  98.         frontend.inMainThread(lambda:eventloop.addUrgentCall(func, name))
  99.     except:
  100.         eventloop.addIdle(func, name)
  101.  
  102. def queueSelectDisplay(frame, display, area):
  103.     """Queue a call to MainFrame.selectDisplay using queueDOMChange.  This is
  104.     useful if you want it to happen after template DOM updates (see
  105.     selection.py for an example).
  106.     """
  107.     if area in toSelect:
  108.         # There's already a display queued up. Dispose of it properly
  109.         toSelect[area].unlink()
  110.         
  111.     toSelect[area] = display
  112.     queueDOMChange(lambda: doSelectDisplay(frame, area), "Select display")
  113.  
  114. toSelect = {}
  115. def doSelectDisplay(frame, area):
  116.     if area in toSelect:
  117.         frame.selectDisplay(toSelect.pop(area), area)
  118.         
  119. ###############################################################################
  120. #### template runtime code                                                 ####
  121. ###############################################################################
  122.  
  123. # Class used internally by Handle to track a t:repeatForSet clause.
  124. class TrackedView:
  125.     def __init__(self, anchorId, anchorType, view, templateFunc, parent, name):
  126.         # arguments as Handle.addView(), plus 'parent', a pointer to the Handle
  127.         # that is used to find domHandler and invoke checkHides
  128.         self.anchorId = anchorId
  129.         self.anchorType = anchorType
  130.  
  131.         self.view = view
  132.         self.templateFunc = templateFunc
  133.         self.parent = parent
  134.         self.htmlChanger = parent.htmlChanger
  135.         self.name = name
  136.         self.toChange = {}
  137.         self.toRemove = []
  138.         self.toAdd = []
  139.         self.addBefore = None
  140.         self.idle_queued = False
  141.  
  142.     def tid(self, obj):
  143.         return "objid-%s-%d" % (id(self), id(obj))
  144.  
  145.     #
  146.     # This is called after the HTML has been rendered to fill in the
  147.     # data for each view and register callbacks to keep it updated
  148.     def initialFillIn(self):
  149.         self.view.confirmDBThread()
  150.         self.toChange = {}
  151.         self.toRemove = []
  152.         self.toAdd = []
  153.         self.addBefore = None
  154.  
  155.         #print "Filling in %d items" % self.view.len()
  156.         #start = time.clock()
  157.         self.addObjects(self.view)
  158.         self.view.addChangeCallback(self.onChange)
  159.         self.view.addAddCallback(self.onAdd)
  160.         self.view.addResortCallback(self.onResort)
  161.         self.view.addRemoveCallback(self.onRemove)
  162.         #print "done (%f)" % (time.clock()-start)
  163.  
  164.     def onUnlink(self):
  165.         self.view.removeChangeCallback(self.onChange)
  166.         self.view.removeAddCallback(self.onAdd)
  167.         self.view.removeResortCallback(self.onResort)
  168.         self.view.removeRemoveCallback(self.onRemove)
  169.  
  170.     def initialXML(self, item):
  171.         xml = self.currentXML(item)
  172.         self.htmlChanger.setInitialHTML(self.tid(item), xml)
  173.         return xml
  174.  
  175.     def currentXML(self, item):
  176.         xml = self.templateFunc(item, self.name, self.view, self.tid(item)).read()
  177.         return xml
  178.  
  179.     def callback (self):
  180.         if self.parent.domHandler:
  181.             self.addObjects(self.toAdd)
  182.             changes = []
  183.             for id in self.toChange:
  184.                 obj = self.toChange[id]
  185.                 xml = self.currentXML(obj)
  186.                 changes.extend(self.htmlChanger.calcChanges(self.tid(obj), xml))
  187.             if len(changes) > 0:
  188.                 self.parent.domHandler.changeItems(changes)
  189.  
  190.             tids = [self.tid(obj) for obj in self.toRemove]
  191.             if len(tids) > 0:
  192.                 self.htmlChanger.removeElements(tids)
  193.                 self.parent.domHandler.removeItems(tids)
  194.             
  195.         self.toChange = {}
  196.         self.toRemove = []
  197.         self.toAdd = []
  198.         self.addBefore = None
  199.         self.idle_queued = False
  200.  
  201.     def addCallback(self):
  202.         if not self.idle_queued:
  203.             queueDOMChange(self.callback, "TrackedView DOM Change (%s)" % self.name)
  204.             self.idle_queued = True
  205.  
  206.     def onResort (self):
  207.         self.toChange = {}
  208.         self.toRemove = []
  209.         self.toAdd = []
  210.         self.addBefore = None
  211.  
  212.         if self.anchorType == 'containerDiv':
  213.             emptyXML = '<div id="%s"></div>' % (self.anchorId, )
  214.             self.parent.domHandler.changeItem(self.anchorId, emptyXML, None)
  215.         else:
  216.             removeTids = [self.tid(obj) for obj in self.view]
  217.             if len(removeTids) > 0:
  218.                 self.parent.domHandler.removeItems(removeTids)
  219.         self.addObjects(self.view)
  220.  
  221.     def onChange(self,obj,id):
  222.         if obj in self.toAdd:
  223.             return
  224.         self.toChange[id] = obj
  225.         self.addCallback()
  226.  
  227.     def onAdd(self, obj, id):
  228.         if len(self.toChange) > 0 or len(self.toRemove) > 0:
  229.             self.callback()
  230.         if self.parent.domHandler:
  231.             next = self.view.getNextID(id) 
  232.             if next is not None:
  233.                 nextTid = self.tid (self.view.getObjectByID(next))
  234.             else:
  235.                 nextTid = None
  236.             for i in range(len(self.toAdd)):
  237.                 if self.tid(self.toAdd[i]) == nextTid:
  238.                     self.toAdd.insert(i, obj)
  239.                     self.addCallback()
  240.                     return
  241.             if len(self.toAdd) > 0 and nextTid == self.addBefore:
  242.                 self.toAdd.append(obj)
  243.                 self.addCallback()
  244.             else:
  245.                 self.callback()
  246.                 self.toAdd.append(obj)
  247.                 self.addBefore = nextTid
  248.                 self.addCallback()
  249.  
  250.     def addObjects(self, objects):
  251.         """Insert the XML for a list of objects into the DOM tree."""
  252.  
  253.         if len(objects) == 0:
  254.             # Web Kit treats adding the empty string like adding " ", so
  255.             # we don't add the HTML unless it's non-empty
  256.             return
  257.         xmls = [self.initialXML(x) for x in objects]
  258.         # only render with 100 picees at a time, otherwise we can end up
  259.         # trying to allocate huge strings (#8320)
  260.         for xmls_part in util.partition(xmls, 100):
  261.             self.addXML(''.join(xmls_part))
  262.  
  263.     def addXML(self, xml):
  264.         # Adding it at the end of the list. Must add it relative to
  265.         # the anchor.
  266.  
  267.         if self.addBefore:
  268.             self.parent.domHandler.addItemBefore(xml, self.addBefore)
  269.         else:
  270.             if self.anchorType in ('parentNode', 'containerDiv'):
  271.                 self.parent.domHandler.addItemAtEnd(xml, self.anchorId)
  272.             if self.anchorType == 'nextSibling':
  273.                 self.parent.domHandler.addItemBefore(xml, self.anchorId)
  274.  
  275.     def onRemove (self, obj, id):
  276.         if obj in self.toAdd:
  277.             self.toAdd.remove(obj)
  278.             return
  279.         if len (self.toAdd) > 0:
  280.             self.callback()
  281.         if id in self.toChange:
  282.             del self.toChange[id]
  283.         self.toRemove.append(obj)
  284.         self.addCallback()
  285.  
  286. # UpdateRegion and ConfigUpdateRegion are used internally by Handle to track
  287. # the t:updateForView and t:updateForConfigChange clauses.
  288.  
  289. class UpdateRegionBase:
  290.     """Base class for UpdateRegion and ConfigUpdateRegion.  Subclasses must
  291.     define renderXML, which returns a string representing the up-to-date XML
  292.     for this region.  Also, hookupCallbacks() which hooks up any callbacks
  293.     needed.  Subclasses can use onChange() as the handler for any callbacks.
  294.     """
  295.  
  296.     def __init__(self, anchorId, anchorType, templateFunc, parent):
  297.         # arguments as Handle.addView(), plus 'parent', a pointer to the Handle
  298.         # that is used to find domHandler and invoke checkHides
  299.         self.anchorId = anchorId
  300.         self.anchorType = anchorType
  301.  
  302.         self.templateFunc = templateFunc
  303.         self.parent = parent
  304.         self.htmlChanger = self.parent.htmlChanger
  305.         self.tid = generateId()
  306.         self.idle_queued = False
  307.  
  308.     #
  309.     # This is called after the HTML has been rendered to fill in the
  310.     # data for each view and register callbacks to keep it updated
  311.     def initialFillIn(self):
  312.         if self.parent.domHandler:
  313.             self.parent.domHandler.addItemBefore(self.initialXML(), self.anchorId)
  314.         self.hookupCallbacks()
  315.  
  316.     def initialXML(self):
  317.         xml = self.renderXML()
  318.         self.htmlChanger.setInitialHTML(self.tid, xml)
  319.         return xml
  320.  
  321.     def onChange(self, *args, **kwargs):
  322.         if not self.idle_queued:
  323.             queueDOMChange(self.doChange, "UpdateRegion DOM Change (%s)" % self.name)
  324.             self.idle_queued = True
  325.  
  326.     def doChange(self):
  327.         xml = self.renderXML()
  328.         changes = self.parent.htmlChanger.calcChanges(self.tid, xml)
  329.         if changes and self.parent.domHandler:
  330.             self.parent.domHandler.changeItems(changes)
  331.         self.idle_queued = False
  332.  
  333. class UpdateRegion(UpdateRegionBase):
  334.     def __init__(self, anchorId, anchorType, view, templateFunc, parent, name):
  335.         UpdateRegionBase.__init__(self, anchorId, anchorType, templateFunc, parent)
  336.         self.view = view
  337.         self.name = name
  338.  
  339.     def renderXML(self):
  340.         return self.templateFunc(self.name, self.view, self.tid).read()
  341.  
  342.     def hookupCallbacks(self):
  343.         self.view.addChangeCallback(self.onChange)
  344.         self.view.addAddCallback(self.onChange)
  345.         self.view.addRemoveCallback(self.onChange)
  346.         self.view.addResortCallback(self.onChange)
  347.         self.view.addViewChangeCallback(self.onChange)
  348.  
  349.     def onUnlink(self):
  350.         self.view.removeChangeCallback(self.onChange)
  351.         self.view.removeAddCallback(self.onChange)
  352.         self.view.removeRemoveCallback(self.onChange)
  353.         self.view.removeResortCallback(self.onChange)
  354.         self.view.removeViewChangeCallback(self.onChange)
  355.  
  356. class ConfigUpdateRegion(UpdateRegionBase):
  357.     def __init__(self, anchorId, anchorType, templateFunc, parent):
  358.         UpdateRegionBase.__init__(self, anchorId, anchorType, templateFunc, parent)
  359.         self.name = "ConfigUpdateRegion"
  360.     def hookupCallbacks(self):
  361.         config.addChangeCallback(self.onChange)
  362.  
  363.     def onUnlink(self):
  364.         config.removeChangeCallback(self.onChange)
  365.  
  366.     def renderXML(self):
  367.         return self.templateFunc(self.tid).read()
  368.  
  369. # Object representing a set of registrations for Javascript callbacks when
  370. # the contents of some set of database views change. One of these Handles
  371. # is returned whenever you fill a template; when you no longer want to
  372. # receive Javascript callbacks for a particular filled template, call
  373. # this object's unlinkTemplate() method.
  374. #
  375. # localVars is a dictionary of variables associated with this template
  376. class Handle:
  377.     def __init__(self, domHandler, templateVars, document = None, onUnlink = lambda : None):        
  378.         # 'domHandler' is an object that will receive method calls when
  379.         # dynamic page updates need to be made. 
  380.         self.domHandler = domHandler
  381.         self.templateVars = templateVars
  382.         self.document = document
  383.         self.trackedHides = {}
  384.         self.trackedViews = []
  385.         self.updateRegions = []
  386.         self.subHandles = []
  387.         self.triggerActionURLsOnLoad = []
  388.         self.triggerActionURLsOnUnload = []
  389.         self.onUnlink = onUnlink
  390.         self.htmlChanger = HTMLChangeOptimizer()
  391.         self.filled = False
  392.  
  393.     def addTriggerActionURLOnLoad(self,url):
  394.         self.triggerActionURLsOnLoad.append(str(url))
  395.  
  396.     def addTriggerActionURLOnUnload(self, url):
  397.         self.triggerActionURLsOnUnload.append(str(url))
  398.  
  399.     def getTriggerActionURLsOnLoad(self):
  400.         return self.triggerActionURLsOnLoad
  401.  
  402.     def getTriggerActionURLsOnUnload(self):
  403.         return self.triggerActionURLsOnUnload
  404.  
  405.     def getTemplateVariable(self, name):
  406.         return self.templateVars[name]
  407.  
  408.     def addUpdateHideOnView(self, id, view, hideFunc, previous):
  409.         checkFunc = lambda *args: self._checkHide(id)
  410.         self.trackedHides[id] = (view, hideFunc, checkFunc, previous)
  411.         if self.filled:
  412.             self.addHideChecks(view, checkFunc)
  413.  
  414.     def _checkHide(self, id):
  415.         (view, hideFunc, checkFunc, previous) = self.trackedHides[id]
  416.         if hideFunc() != previous:
  417.             self.trackedHides[id] = (view, hideFunc, checkFunc, not previous)
  418.             if previous: # If we were hidden, show
  419.                 self.domHandler.showItem(id)
  420.             else:        # If we were showing it, hide it
  421.                 self.domHandler.hideItem(id)
  422.  
  423.  
  424.     def addView(self, anchorId, anchorType, view, templateFunc, name):
  425.         # Register for JS calls to populate a t:repeatFor. 'view' is the
  426.         # database view to track; 'node' is a DOM node representing the
  427.         # template to fill; 'data' are extra variables to be used in expanding
  428.         # the template. The 'anchor*' arguments tell where in the document
  429.         # to place the expanded template nodes. If 'anchorType' is
  430.         # 'nextSibling', 'anchorId' is the id attribute of the tag immediately
  431.         # following the place the template should be expanded. If it is
  432.         # 'parentNode', the template should be expanded so as to be the final
  433.         # child in the node whose id attribute matches 'anchorId'.
  434.         # 'containerDiv' is like parentNode, except it's contained in an
  435.         # auto-generated <div> element.  This allows for efficient changes
  436.         # when the view is re-sorted.
  437.         #
  438.         # We take a private copy of 'node', so don't worry about modifying
  439.         # it subsequent to calling this method.
  440.         tv = TrackedView(anchorId, anchorType, view, templateFunc, self, name)
  441.         self.trackedViews.append(tv)
  442.  
  443.     def addUpdate(self, anchorId, anchorType, view, templateFunc, name):
  444.         ur = UpdateRegion(anchorId, anchorType, view, templateFunc, self, name)
  445.         self.updateRegions.append(ur)
  446.  
  447.     # This forces all "update for view" sections to update
  448.     def forceUpdate(self):
  449.         for ur in self.updateRegions:
  450.             ur.onChange()
  451.         for h in self.subHandles:
  452.             h.forceUpdate()
  453.  
  454.     def addConfigUpdate(self, anchorId, anchorType, templateFunc):
  455.         ur = ConfigUpdateRegion(anchorId, anchorType, templateFunc, self)
  456.         self.updateRegions.append(ur)
  457.  
  458.     def unlinkTemplate(self, top = True):
  459.         # Stop delivering callbacks, allowing the handle to be released.
  460.         self.domHandler = None
  461.         try:
  462.             self.document.unlink()
  463.         except:
  464.             pass
  465.         self.document = None
  466.         for o in chain(self.trackedViews, self.updateRegions):
  467.             o.onUnlink()
  468.         self.trackedViews = []
  469.         self.updateRegions = []
  470.         if self.filled:
  471.             for id in self.trackedHides.keys():
  472.                 (view, hideFunc, checkFunc, previous) = self.trackedHides[id]
  473.                 self.removeHideChecks(view, checkFunc)
  474.         self.trackedHides = {}
  475.         for handle in self.subHandles:
  476.             handle.unlinkTemplate()
  477.         self.subHandles = []
  478.         self.templateVars.clear()
  479.         self.onUnlink()
  480.  
  481.     def initialFillIn(self):
  482.         for ur in self.updateRegions:
  483.             ur.initialFillIn()
  484.         for tv in self.trackedViews:
  485.             tv.initialFillIn()
  486.         for handle in self.subHandles:
  487.             handle.initialFillIn()
  488.         for id in self.trackedHides.keys():
  489.             (view, hideFunc, checkFunc, previous) = self.trackedHides[id]
  490.             self.addHideChecks(view, checkFunc)
  491.         self.filled = True
  492.  
  493.     def addHideChecks(self, view, checkFunc):
  494.         logging.debug ("Add hide checks: function %s on view %s", checkFunc, view)
  495.         view.addChangeCallback(checkFunc)
  496.         view.addAddCallback(checkFunc)
  497.         view.addRemoveCallback(checkFunc)
  498.         view.addResortCallback(checkFunc)
  499.         view.addViewChangeCallback(checkFunc)
  500.         checkFunc()
  501.  
  502.     def removeHideChecks(self, view, checkFunc):
  503.         logging.debug ("Remove hide checks: function %s on view %s", checkFunc, view)
  504.         view.removeChangeCallback(checkFunc)
  505.         view.removeAddCallback(checkFunc)
  506.         view.removeRemoveCallback(checkFunc)
  507.         view.removeResortCallback(checkFunc)
  508.         view.removeViewChangeCallback(checkFunc)
  509.  
  510.     def addSubHandle(self, handle):
  511.         self.subHandles.append(handle)
  512.  
  513. # Random utility functions 
  514. def returnFalse(x):
  515.     return False
  516.  
  517. def returnTrue(x):
  518.     return True
  519.  
  520. def identityFunc(x):
  521.     return x
  522.  
  523. def nullSort(x,y):
  524.     return 0
  525.  
  526. # Returns a quoted, filled version of attribute text
  527. def quoteAndFillAttr(value, localVars):
  528.     checkU(value)
  529.     return ''.join(('"',quoteattr(fillAttr(value, localVars)),'"'))
  530.  
  531. # Returns a filled version of attribute text
  532. # Important: because we expand resource: URLs here, instead of defining a
  533. # URL handler (which is hard to do in IE), you must link to stylesheets via
  534. # <link .../> rather than <style> @import ... </style> if they are resource:
  535. # URLs.
  536.  
  537. # FIXME: we should parse the attribute values ahead of time
  538. def fillAttr(_value, _localVars):
  539.     checkU(_value)
  540.     match = attrPattern.match(_value)
  541.     if match:
  542.         result = eval(match.group(2), globals(), _localVars)
  543.         return ''.join((match.group(1), urlencode(result), match.group(3)))
  544.     else:
  545.         match = rawAttrPattern.match(_value)
  546.         if match:
  547.             result = eval(match.group(2), globals(), _localVars)
  548.             return ''.join((match.group(1), result, match.group(3)))
  549.         else:
  550.             match = resourcePattern.match(_value)
  551.             if match:
  552.                 return resources.url(match.group(1))
  553.             else:
  554.                 return _value
  555.  
  556. # This has to be after Handle, so the compiled templates can get
  557. # access to Handle
  558. import compiled_templates
  559.